/*
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
#include "odin_types.h"
#include "odin_globals.h"
#include "memories.h"
#include "hard_blocks.h"
#include "implicit_memory.h"
#include "node_creation_library.h"
#include "netlist_utils.h"
#include "odin_util.h"
#include "vtr_util.h"
#include "vtr_memory.h"

#include <unordered_map>

// Hashes the implicit memory name to the implicit_memory structure.
std::unordered_map<std::string, implicit_memory*> implicit_memories;
// Hashes the implicit memory input name to the implicit_memory structure.
std::unordered_map<std::string, implicit_memory*> implicit_memory_inputs;

void finalize_implicit_memory(implicit_memory* memory);
void add_dummy_output_port_to_implicit_memory(implicit_memory* memory, int size, const char* port_name);
void add_dummy_input_port_to_implicit_memory(implicit_memory* memory, int size, const char* port_name);
void collapse_implicit_memory_to_single_port_ram(implicit_memory* memory);
implicit_memory* lookup_implicit_memory(char* instance_name_prefix, char* identifier);

/*
 * Initialises hashtables to lookup memories based on inputs and names.
 */
void init_implicit_memory_index() {
    implicit_memories = std::unordered_map<std::string, implicit_memory*>();
    implicit_memory_inputs = std::unordered_map<std::string, implicit_memory*>();
}

/*
 * Looks up an implicit memory by identifier name in the implicit memory lookup table.
 */
implicit_memory* lookup_implicit_memory(char* instance_name_prefix, char* identifier) {
    char* memory_string = make_full_ref_name(instance_name_prefix, NULL, NULL, identifier, -1);

    std::unordered_map<std::string, implicit_memory*>::const_iterator mem_out = implicit_memories.find(std::string(memory_string));

    vtr::free(memory_string);

    if (mem_out == implicit_memories.end())
        return NULL;
    else
        return mem_out->second;
}

/*
 * Looks up an implicit memory by ast reference in the implicit memory lookup table.
 */
implicit_memory* lookup_implicit_memory_reference_ast(char* instance_name_prefix, ast_node_t* node) {
    if (node && node->num_children == 2 && node->type == ARRAY_REF)
        return lookup_implicit_memory(instance_name_prefix, node->children[0]->types.identifier);
    else if (node && node->num_children == 3 && node->type == ARRAY_REF)
        return lookup_implicit_memory(instance_name_prefix, node->children[0]->types.identifier);
    else if (node && node->type == IDENTIFIERS)
        return lookup_implicit_memory(instance_name_prefix, node->types.identifier);
    else
        return NULL;
}

/*
 * Determines if the given implicit memory reference mode is supported.
 */
char is_valid_implicit_memory_reference_ast(char* instance_name_prefix, ast_node_t* node) {
    if (node && node->num_children == 2 && node->type == ARRAY_REF
        && lookup_implicit_memory_reference_ast(instance_name_prefix, node))
        return true;
    else if (node && node->num_children == 3 && node->type == ARRAY_REF
             && lookup_implicit_memory_reference_ast(instance_name_prefix, node))
        return true;
    else
        return false;
}

/*
 * Creates an implicit memory block with the given depth and data width, and the given name and prefix.
 */
implicit_memory* create_implicit_memory_block(int data_width, long memory_depth, char* name, char* instance_name_prefix) {
    char implicit_string[] = "implicit_ram";

    oassert(memory_depth > 0
            && "implicit memory depth must be greater than 0");

    //find closest power of 2 from memory depth.
    long addr_width = 0;
    long real_memory_depth = 1;
    while (real_memory_depth < memory_depth) {
        addr_width += 1;
        real_memory_depth = shift_left_value_with_overflow_check(real_memory_depth, 0x1);
    }

    //verify if it is a power of two (only one bit set)
    if ((memory_depth != real_memory_depth)) {
        warning_message(NETLIST, -1, -1, "Rounding memory <%s> of size <%ld> to closest power of two: %ld.", name, memory_depth, real_memory_depth);
        memory_depth = real_memory_depth;
    }

    nnode_t* node = allocate_nnode();
    node->type = MEMORY;
    node->name = hard_node_name(node, instance_name_prefix, implicit_string, name);

    // Create a fake ast node.
    node->related_ast_node = (ast_node_t*)vtr::calloc(1, sizeof(ast_node_t));
    node->related_ast_node->children = (ast_node_t**)vtr::calloc(1, sizeof(ast_node_t*));
    node->related_ast_node->children[0] = (ast_node_t*)vtr::calloc(1, sizeof(ast_node_t));
    node->related_ast_node->children[0]->types.identifier = vtr::strdup(DUAL_PORT_RAM_string);

    char* full_name = make_full_ref_name(instance_name_prefix, NULL, NULL, name, -1);

    implicit_memory* memory = (implicit_memory*)vtr::malloc(sizeof(implicit_memory));
    memory->node = node;
    memory->addr_width = addr_width;
    memory->memory_depth = memory_depth;
    memory->data_width = data_width;
    memory->clock_added = false;
    memory->output_added = false;
    memory->name = full_name;

    implicit_memories.insert({std::string(full_name), memory});

    return memory;
}

/*
 * Adds an input port to the given implicit memory.
 */
void add_input_port_to_implicit_memory(implicit_memory* memory, signal_list_t* signals, const char* port_name) {
    nnode_t* node = memory->node;

    add_input_port_to_memory(node, signals, port_name);
}

/*
 * Add an output port to the given implicit memory.
 */
void add_output_port_to_implicit_memory(implicit_memory* memory, signal_list_t* signals, const char* port_name) {
    nnode_t* node = memory->node;

    add_output_port_to_memory(node, signals, port_name);
}

/*
 * Looks up an implicit memory based on the given name.
 */
implicit_memory* lookup_implicit_memory_input(char* name) {
    std::unordered_map<std::string, implicit_memory*>::const_iterator mem_out = implicit_memory_inputs.find(std::string(name));

    if (mem_out == implicit_memory_inputs.end())
        return NULL;
    else
        return mem_out->second;
}

/*
 * Registers the given input name so that the given memory can be looked up based on
 * it.
 */
void register_implicit_memory_input(char* name, implicit_memory* memory) {
    if (!lookup_implicit_memory_input(name))
        implicit_memory_inputs.insert({std::string(name), memory});
    else
        error_message(NETLIST, -1, -1, "Attempted to re-register implicit memory output %s.", name);
}

/*
 * Frees memory used for indexing implicit memories. Finalises each
 * memory, making sure it has the right ports, and collapsing
 * the memory if possible.
 */
void free_implicit_memory_index_and_finalize_memories() {
    implicit_memory_inputs.clear();

    if (!implicit_memories.empty()) {
        for (auto mem_it : implicit_memories) {
            finalize_implicit_memory(mem_it.second);
            vtr::free(mem_it.second->name);
            vtr::free(mem_it.second);
        }
    }
    implicit_memories.clear();
}

/*
 * Adds a zeroed input port with to the given implicit memory
 * with the given size and port name (mapping)
 */
void add_dummy_input_port_to_implicit_memory(implicit_memory* memory, int size, const char* port_name) {
    signal_list_t* signals = init_signal_list();
    int i;
    for (i = 0; i < size; i++)
        add_pin_to_signal_list(signals, get_zero_pin(verilog_netlist));

    add_input_port_to_implicit_memory(memory, signals, port_name);

    free_signal_list(signals);
}

/*
 * Adds an unconnected output port with to the given implicit memory
 * with the given size and port name (mapping)
 */
void add_dummy_output_port_to_implicit_memory(implicit_memory* memory, int size, const char* port_name) {
    signal_list_t* signals = init_signal_list();
    static int dummy_output_pin_number = 0;
    int i;
    for (i = 0; i < size; i++) {
        npin_t* dummy_pin = allocate_npin();
        // Pad outputs with a unique and descriptive name to avoid collisions.
        dummy_pin->name = append_string("", "dummy_implicit_memory_output~%d", dummy_output_pin_number++);
        add_pin_to_signal_list(signals, dummy_pin);
    }

    add_output_port_to_implicit_memory(memory, signals, port_name);

    free_signal_list(signals);
}

/*
 * Makes sure the given implicit memory has all necessary ports,
 * and adds any ports which may be missing. Collapses the memory to
 * a single port ram if one port is unused.
 */
void finalize_implicit_memory(implicit_memory* memory) {
    nnode_t* node = memory->node;

    bool has_addr1 = false;
    bool has_addr2 = false;
    bool has_data1 = false;
    bool has_data2 = false;
    bool has_we1 = false;
    bool has_we2 = false;
    bool has_clk = false;
    bool has_out1 = false;
    bool has_out2 = false;

    // Determine which input ports are present.
    int i;
    for (i = 0; i < node->num_input_pins; i++) {
        npin_t* pin = node->input_pins[i];
        if (!strcmp(pin->mapping, "addr1"))
            has_addr1 = true;
        else if (!strcmp(pin->mapping, "addr2"))
            has_addr2 = true;
        else if (!strcmp(pin->mapping, "data1"))
            has_data1 = true;
        else if (!strcmp(pin->mapping, "data2"))
            has_data2 = true;
        else if (!strcmp(pin->mapping, "we1"))
            has_we1 = true;
        else if (!strcmp(pin->mapping, "we2"))
            has_we2 = true;
        else if (!strcmp(pin->mapping, "clk"))
            has_clk = true;
    }

    // Determine which output ports are present.
    for (i = 0; i < node->num_output_pins; i++) {
        npin_t* pin = node->output_pins[i];
        if (!strcmp(pin->mapping, "out1"))
            has_out1 = true;
        else if (!strcmp(pin->mapping, "out2"))
            has_out2 = true;
    }

    if (!has_clk) {
        add_dummy_input_port_to_implicit_memory(memory, 1, "clk");
        warning_message(NETLIST, -1, -1, "Implicit memory %s is not clocked. Padding clock pin.", memory->name);
    }

    char has_port1 = has_addr1 || has_data1 || has_we1 || has_out1;
    char has_port2 = has_addr2 || has_data2 || has_we2 || has_out2;

    if (has_port1) {
        if (!has_addr1) add_dummy_input_port_to_implicit_memory(memory, memory->addr_width, "addr1");
        if (!has_data1) add_dummy_input_port_to_implicit_memory(memory, memory->data_width, "data1");
        if (!has_we1) add_dummy_input_port_to_implicit_memory(memory, 1, "we1");
        if (!has_out1) add_dummy_output_port_to_implicit_memory(memory, memory->data_width, "out1");
    }

    if (has_port2) {
        if (!has_addr2) add_dummy_input_port_to_implicit_memory(memory, memory->addr_width, "addr2");
        if (!has_data2) add_dummy_input_port_to_implicit_memory(memory, memory->data_width, "data2");
        if (!has_we2) add_dummy_input_port_to_implicit_memory(memory, 1, "we2");
        if (!has_out2) add_dummy_output_port_to_implicit_memory(memory, memory->data_width, "out2");
    }

    if (!has_port1 || !has_port2)
        collapse_implicit_memory_to_single_port_ram(memory);

    if (!has_port1 && !has_port2) {
        warning_message(NETLIST, -1, -1, "Implicit memory %s has no ports...", memory->name);
    } else {
        /*
         *  If this hard block is supported, register it globally and mark
         *  it as used. (For splitting and BLIF output.)
         *
         *  If it isn't supported, it will be automagically blown out
         *  into soft logic during the partial map.
         */
        ast_node_t* ast_node = node->related_ast_node;
        char* hard_block_identifier = ast_node->children[0]->types.identifier;
        t_model* hb_model = find_hard_block(hard_block_identifier);
        if (hb_model) {
            hb_model->used = 1;
            if (!strcmp(hard_block_identifier, SINGLE_PORT_RAM_string))
                sp_memory_list = insert_in_vptr_list(sp_memory_list, node);
            else
                dp_memory_list = insert_in_vptr_list(dp_memory_list, node);
        }
    }
}

/*
 * Turns the given implicit memory into a single port ram from the
 * default dual port ram. This is a useful optimisation when one port is unused.
 *
 * All implicit memories are constructed initially as
 * dual port rams.
 */
void collapse_implicit_memory_to_single_port_ram(implicit_memory* memory) {
    nnode_t* node = memory->node;

    // Change the inputs to single port ram mappings by removing
    // the port numbers (1 or 2) from the mappings. (last char)
    int i;
    for (i = 0; i < node->num_input_pins; i++) {
        npin_t* pin = node->input_pins[i];
        if (strcmp(pin->mapping, "clk"))
            pin->mapping[strlen(pin->mapping) - 1] = 0;
    }

    // Change the outputs to single port ram mappings by removing
    // the port numbers. (last char)
    for (i = 0; i < node->num_output_pins; i++) {
        npin_t* pin = node->output_pins[i];
        pin->mapping[strlen(pin->mapping) - 1] = 0;
    }

    ast_node_t* ast_node = node->related_ast_node;
    vtr::free(ast_node->children[0]->types.identifier);
    ast_node->children[0]->types.identifier = vtr::strdup(SINGLE_PORT_RAM_string);
}
